package net.fornwall.eclipsecoder.javasupport;

import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.fornwall.eclipsecoder.javasupport.preferences.JavaSupportPlugin;
import net.fornwall.eclipsecoder.languages.LanguageSupport;
import net.fornwall.eclipsecoder.stats.CodeGenerator;
import net.fornwall.eclipsecoder.stats.ProblemStatement;
import net.fornwall.eclipsecoder.util.Utilities;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.Comment;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jface.text.Document;
import org.eclipse.text.edits.TextEdit;

/**
 * For more information on the JDT java model, see:
 * http://www.eclipsecon.org/2005/presentations/EclipseCON2005_Tutorial29.pdf
 */
public class JavaLanguageSupport extends LanguageSupport {

	public static final String DEFAULT_CODE_TEMPLATE = "public class "
			+ LanguageSupport.CLASSNAME_TAG + " {\n\n" + "    public "
			+ LanguageSupport.RETURNTYPE_TAG + " "
			+ LanguageSupport.METHODNAME_TAG + "("
			+ LanguageSupport.METHODPARAMS_TAG + ") {\n" + "        return "
			+ LanguageSupport.DUMMYRETURN_TAG + ";\n" + "    }\n\n" + "}\n";

	/**
	 * Utility method for getting the AST parser for source.
	 * 
	 * @param source
	 *            the java 1.5 compatible source code
	 * @return a parser of the supplied source code
	 */
	@SuppressWarnings("unchecked")
	private static ASTParser getParser(String source) {
		Map<String, String> compilerOptions = new HashMap<String, String>(); // JavaCore.getOptions();
		compilerOptions.put(JavaCore.COMPILER_SOURCE, JavaCore.VERSION_1_5);
		compilerOptions.put(JavaCore.COMPILER_COMPLIANCE, JavaCore.VERSION_1_5);

		ASTParser parser = ASTParser.newParser(AST.JLS3);
		parser.setKind(ASTParser.K_COMPILATION_UNIT);
		parser.setSource(source.toCharArray());
		parser.setResolveBindings(true);
		parser.setCompilerOptions(compilerOptions);
		return parser;
	}

	/**
	 * Parse java source code into a AST tree.
	 * 
	 * @param source
	 *            the java source code
	 * @return the AST tree for the java source code
	 */
	static CompilationUnit parse(String source) {
		return (CompilationUnit) getParser(source).createAST(null);
	}

	/**
	 * Strip all comments from the source.
	 * 
	 * The comments returned in getCommentList() are not positioned in the AST
	 * and may not be deleted by calling delete(). So this is a (temporary?)
	 * workaround.
	 * 
	 * @param source
	 *            The java source which should be stripped from comments.
	 * @return The source stripped from comments.
	 */
	@SuppressWarnings("unchecked")
	private static String stripComments(String source) {
		CompilationUnit compilationUnit = (CompilationUnit) getParser(source)
				.createAST(null);
		List<Comment> commentList = compilationUnit.getCommentList();
		int lastPos = 0;
		StringBuffer buffer = new StringBuffer();
		for (Comment comment : commentList) {
			// comment.delete() does not work as a Comment is unparented
			buffer
					.append(source.substring(lastPos, comment
							.getStartPosition()));
			lastPos = comment.getStartPosition() + comment.getLength();
		}
		buffer.append(source.substring(lastPos, source.length()));
		return buffer.toString();
	}

	@Override
	protected CodeGenerator createCodeGenerator(
			ProblemStatement problemStatement) {
		return new JavaCodeGenerator(problemStatement);
	}

	@Override
	public IFile createLanguageProject(IProject project) throws CoreException,
			JavaModelException {
		IWorkspace workspace = ResourcesPlugin.getWorkspace();
		IJavaProject javaProject = JavaCore.create(project);

		IProjectDescription newProjectDescription = workspace
				.newProjectDescription(project.getName());
		newProjectDescription.setNatureIds(new String[] { JavaCore.NATURE_ID });
		project.setDescription(newProjectDescription, null);

		IClasspathEntry sourceEntry = JavaCore.newSourceEntry(javaProject.getPath());
		IClasspathEntry conEntry = JavaCore.newContainerEntry(new Path(
				JavaRuntime.JRE_CONTAINER + "/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5"));
		IClasspathEntry junitEntry = JavaCore.newContainerEntry(new Path(
				"org.eclipse.jdt.junit.JUNIT_CONTAINER/4"));
		javaProject.setRawClasspath(new IClasspathEntry[] { sourceEntry,
				conEntry, junitEntry }, null);

		// current topcoder setup uses java 1.5:
		// http://www.topcoder.com/tc?module=Static&d1=help&d2=generalFaq#java3
		final String JAVA_VERSION = JavaCore.VERSION_1_5;
		javaProject.setOption(JavaCore.COMPILER_COMPLIANCE, JAVA_VERSION);
		javaProject.setOption(JavaCore.COMPILER_SOURCE, JAVA_VERSION);
		javaProject.setOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, JAVA_VERSION);

		// create an ant build file - eclipsecoder does not use it, but may be
		// used from external programs or command line
		IFile buildFile = project.getFile("build.xml");
		buildFile
				.create(
						new ByteArrayInputStream(
								("<project name=\"Sample.Project\" default=\"runtests\" basedir=\".\">\r\n"
										+ "    <property name=\"app.name\"   value=\"sample\" />\r\n"
										+ "    <property name=\"build.dir\"  value=\"bin\" />\r\n"
										+ "\r\n"
										+ "    <target name=\"compiletests\">\r\n"
										+ "        <javac srcdir=\".\" destdir=\"${build.dir}\" debug=\"true\" debuglevel=\"lines,vars,source\">\r\n"
										+ "            <include name=\"*.java\"/>\r\n"
										+ "        </javac>\r\n"
										+ "    </target>\r\n"
										+ "\r\n"
										+ "    <target name=\"runtests\" depends=\"compiletests\">\r\n"
										+ "        <java fork=\"yes\" classname=\"junit.textui.TestRunner\" taskname=\"junit\" failonerror=\"true\">\r\n"
										+ "            <arg value=\""
										+ getProblemStatement()
												.getSolutionClassName()
										+ "Test\"/>\r\n"
										+ "            <classpath>\r\n"
										+ "                <pathelement location=\"${build.dir}\" />\r\n"
										+ "                <pathelement path=\".\" />\r\n"
										+ "                <pathelement path=\"${java.class.path}\" />\r\n"
										+ "            </classpath>\r\n"
										+ "        </java>\r\n"
										+ "    </target>\r\n"
										+ "\r\n"
										+ "    <target name=\"guitests\" depends=\"compiletests\">\r\n"
										+ "        <java fork=\"yes\" classname=\"junit.swingui.TestRunner\" taskname=\"junit\" failonerror=\"true\">\r\n"
										+ "            <arg value=\""
										+ getProblemStatement()
												.getSolutionClassName()
										+ "Test\"/>\r\n"
										+ "            <classpath>\r\n"
										+ "                <pathelement location=\"${build.dir}\" />\r\n"
										+ "                <pathelement path=\".\" />\r\n"
										+ "                <pathelement path=\"${java.class.path}\" />\r\n"
										+ "            </classpath>\r\n"
										+ "        </java>\r\n"
										+ "    </target>\r\n" + "\r\n" + "</project>\r\n")
										.getBytes()), false, null);

		IFile testsFile = null;
		testsFile = project.getFile(getProblemStatement()
				.getSolutionClassName()
				+ "Test.java");
		testsFile.create(new ByteArrayInputStream(getCodeGenerator()
				.getTestsSource().getBytes()), false, null);

		IFile sourceFile = project.getFile(getSolutionFileName());
		sourceFile.create(new ByteArrayInputStream(getInitialSource()
				.getBytes()), false, null);

		IJavaElement javaElement = javaProject.findElement(new Path(
				getProblemStatement().getSolutionClassName() + "Test.java"));

		if (javaElement != null) {
			// Run initial JUnit test run
			ICompilationUnit compilationUnit = (ICompilationUnit) javaElement;
			Launcher launcher = new Launcher(compilationUnit);
			launcher.run();
		}

		return sourceFile;
	}

	/**
	 * @see net.fornwall.eclipsecoder.languages.LanguageSupport#getCodeEditorID()
	 */
	@Override
	public String getCodeEditorID() {
		return JavaUI.ID_CU_EDITOR;
	}

	@Override
	public String getCodeTemplate() {
		return JavaSupportPlugin.getInstance().getCodeTemplate();
	}

	/**
	 * @see net.fornwall.eclipsecoder.languages.LanguageSupport#getLanguageName()
	 */
	@Override
	public String getLanguageName() {
		return LanguageSupport.JAVA_LANGUAGE_NAME;
	}

	/**
	 * @see net.fornwall.eclipsecoder.languages.LanguageSupport#getPerspectiveID()
	 */
	@Override
	public String getPerspectiveID() {
		return JavaUI.ID_PERSPECTIVE;
	}

	@Override
	protected String getSolutionFileName() {
		return getProblemStatement().getSolutionClassName() + ".java";
	}

	@Override
	public String getSubmission() throws Exception {
		return postProcess(Utilities.getFileContents(getSourceFile()));
	}

	/**
	 * Remove comments and assertions.
	 */
	private String postProcess(String source) {
		try {
			CompilationUnit compilationUnit = (CompilationUnit) getParser(
					source).createAST(null);
			compilationUnit.recordModifications();

			final List<Integer> removeList = new ArrayList<Integer>();
			IJavaProject javaProject = JavaCore.create(getSourceFile()
					.getProject());
			IField[] outFields = {
					javaProject.findType("java.lang.System").getField("out"),
					javaProject.findType("java.lang.System").getField("err") };

			IJavaSearchScope searchScope = SearchEngine
					.createJavaSearchScope(new IJavaElement[] { JavaCore
							.create(getSourceFile()) });

			for (IField field : outFields) {
				SearchPattern searchPattern = SearchPattern.createPattern(
						field, IJavaSearchConstants.REFERENCES);

				// JavaSearchRequestor searchRequestor = new
				// JavaSearchRequestor();
				SearchEngine searchEngine = new SearchEngine();

				searchEngine.search(searchPattern,
						new SearchParticipant[] { SearchEngine
								.getDefaultSearchParticipant() }, searchScope,
						new SearchRequestor() {
							@Override
							public void acceptSearchMatch(SearchMatch match)
									throws CoreException {
								removeList.add(match.getOffset());
							}
						}, null);
			}

			compilationUnit.accept(new Visitor(removeList));
			Document document = new Document(source);
			TextEdit edits = compilationUnit.rewrite(document, null);
			edits.apply(document);
			return stripComments(document.get());
		} catch (Exception e) {
			e.printStackTrace();
		}

		return source;
	}

}
